﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Dapper.Sharding.Maui
{
	public abstract class ISharding<T> where T : class
	{
		public ISharding(ITable<T>[] list)
		{
			if (list[0].SqlField.IsIdentity)
			{
				throw new Exception("auto increment of primary key is not allowed");
			}
			TableList = list;
			Query = new ShardingQuery<T>(TableList);
			SqlField = list[0].SqlField;
			AllClickHouse = TableList.Any(a => a.DbType != DataBaseType.ClickHouse);
		}

		#region base

		public ITable<T>[] TableList { get; }

		public ShardingQuery<T> Query { get; }

		protected SqlFieldEntity SqlField { get; }

		protected bool AllClickHouse { get; }

		protected void Wrap(DistributedTransaction tran, Action action)
		{
			if (tran == null && !AllClickHouse)
			{
				using var tn = new DistributedTransaction();
				action();
				tn.Commit();
			}
			else
			{
				action();
			}
		}

		protected async Task WrapAsync(DistributedTransaction tran, Action action)
		{
			if (tran == null && !AllClickHouse)
			{
#if NET45
				using var tn = new DistributedTransaction();
#else
				await using var tn = new DistributedTransaction();
#endif
				action();
#if NET45
				tn.Commit();
#else
				await tn.CommitAsync();
#endif
			}
			else
			{
				action();
			}
		}

		protected TResult Wrap<TResult>(DistributedTransaction tran, Func<TResult> func)
		{
			if (tran == null && !AllClickHouse)
			{
				using var tn = new DistributedTransaction();
				var result = func();
				tn.Commit();
				return result;
			}
			return func();
		}

		protected async Task<TResult> WrapAsync<TResult>(DistributedTransaction tran, Func<TResult> func)
		{
			if (tran == null && !AllClickHouse)
			{
#if NET45
				using var tn = new DistributedTransaction();
#else
				await using var tn = new DistributedTransaction();
#endif
				var result = func();
#if NET45
				tn.Commit();
#else
				await tn.CommitAsync();
#endif
				return result;
			}
			return func();
		}

		public abstract ITable<T> GetTableById(object id);

		public abstract Task<ITable<T>> GetTableByIdAsync(object id);

		public abstract ITable<T> GetTableByModel(T model);

		public abstract Task<ITable<T>> GetTableByModelAsync(T model);

		public virtual Dictionary<ITable<T>, List<object>> GetTableByGroupIds(object ids)
		{
			var dict = new Dictionary<ITable<T>, List<object>>();
			var idsList = CommonUtil.GetMultiExec(ids);
			if (idsList != null)
			{
				foreach (var id in idsList)
				{
					var table = GetTableById(id);
					if (!dict.ContainsKey(table))
					{
						dict.Add(table, new List<object>());
					}
					dict[table].Add(id);
				}
			}
			return dict;
		}

		public virtual Dictionary<ITable<T>, List<T>> GetTableByGroupModelList(IEnumerable<T> modelList)
		{
			var dict = new Dictionary<ITable<T>, List<T>>();
			foreach (var item in modelList)
			{
				var table = GetTableByModel(item);
				if (!dict.ContainsKey(table))
				{
					dict.Add(table, new List<T>());
				}
				dict[table].Add(item);
			}
			return dict;
		}

		#endregion

		#region insert

		public virtual void Insert(T model, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = GetTableByModel(model);
			tb.Insert(model, tran, timeout);
		}

		public virtual void Insert(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null)
		{
			Wrap(tran, () =>
			 {
				 var dict = GetTableByGroupModelList(modelList);
				 foreach (var item in dict)
				 {
					 item.Key.Insert(item.Value, tran, timeout);
				 }
			 });
		}

		public virtual void InsertIfNoExists(T model, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = GetTableByModel(model);
			tb.InsertIfNoExists(model, tran, timeout);
		}

		public virtual void InsertIfNoExists(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null)
		{
			Wrap(tran, () =>
			 {
				 var dict = GetTableByGroupModelList(modelList);
				 foreach (var item in dict)
				 {
					 item.Key.InsertIfNoExists(item.Value, tran, timeout);
				 }
			 });
		}

		public virtual void Merge(T model, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = GetTableByModel(model);
			tb.Merge(model, fields, tran, timeout);
		}

		public virtual void Merge(IEnumerable<T> modelList, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
		{
			Wrap(tran, () =>
			 {
				 var dict = GetTableByGroupModelList(modelList);
				 foreach (var item in dict)
				 {
					 item.Key.Merge(item.Value, fields, tran, timeout);
				 }
			 });
		}

		public virtual void MergeIgnore(T model, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = GetTableByModel(model);
			tb.MergeIgnore(model, fields, tran, timeout);
		}

		public virtual void MergeIgnore(IEnumerable<T> modelList, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
		{
			Wrap(tran, () =>
			 {
				 var dict = GetTableByGroupModelList(modelList);
				 foreach (var item in dict)
				 {
					 item.Key.MergeIgnore(item.Value, fields, tran, timeout);
				 }
			 });
		}

		#endregion

		#region insertAsync

		public virtual async Task InsertAsync(T model, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = await GetTableByModelAsync(model);
			await tb.InsertAsync(model, tran, timeout);
		}

		public virtual async Task InsertAsync(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null)
		{
			var task = await WrapAsync(tran, async () =>
			{
				var dict = GetTableByGroupModelList(modelList);
				foreach (var item in dict)
				{
					await item.Key.InsertAsync(item.Value, tran, timeout);
				}
			});
			await task;
		}

		public virtual async Task InsertIfNoExistsAsync(T model, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = await GetTableByModelAsync(model);
			await tb.InsertIfNoExistsAsync(model, tran, timeout);
		}

		public virtual async Task InsertIfNoExistsAsync(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null)
		{
			var task = await WrapAsync(tran, async () =>
			{
				var dict = GetTableByGroupModelList(modelList);
				foreach (var item in dict)
				{
					await item.Key.InsertIfNoExistsAsync(item.Value, tran, timeout);
				}
			});
			await task;
		}

		public virtual async Task MergeAsync(T model, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = await GetTableByModelAsync(model);
			await tb.MergeAsync(model, fields, tran, timeout);
		}

		public virtual async Task MergeAsync(IEnumerable<T> modelList, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
		{
			var task = await WrapAsync(tran, async () =>
			{
				var dict = GetTableByGroupModelList(modelList);
				foreach (var item in dict)
				{
					await item.Key.MergeAsync(item.Value, fields, tran, timeout);
				}
			});
			await task;
		}

		public virtual async Task MergeIgnoreAsync(T model, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = await GetTableByModelAsync(model);
			await tb.MergeIgnoreAsync(model, fields, tran, timeout);
		}

		public virtual async Task MergeIgnoreAsync(IEnumerable<T> modelList, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
		{
			var task = await WrapAsync(tran, async () =>
			{
				var dict = GetTableByGroupModelList(modelList);
				foreach (var item in dict)
				{
					await item.Key.MergeIgnoreAsync(item.Value, fields, tran, timeout);
				}
			});
			await task;
		}

		#endregion

		#region update

		public virtual int Update(T model, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = GetTableByModel(model);
			return tb.Update(model, fields, tran, timeout);
		}

		public virtual void Update(IEnumerable<T> modelList, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
		{
			Wrap(tran, () =>
			 {
				 var dict = GetTableByGroupModelList(modelList);
				 foreach (var item in dict)
				 {
					 item.Key.Update(item.Value, fields, tran, timeout);
				 }
			 });
		}

		public virtual int UpdateIgnore(T model, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = GetTableByModel(model);
			return tb.UpdateIgnore(model, fields, tran, timeout);
		}

		public virtual void UpdateIgnore(IEnumerable<T> modelList, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
		{
			Wrap(tran, () =>
			 {
				 var dict = GetTableByGroupModelList(modelList);
				 foreach (var item in dict)
				 {
					 item.Key.UpdateIgnore(item.Value, fields, tran, timeout);
				 }
			 });
		}

		public int UpdateByWhere(T model, string where, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
		{
			return Wrap(tran, () =>
			 {
				 int count = 0;
				 foreach (var item in TableList)
				 {
					 count += item.UpdateByWhere(model, where, fields, tran, timeout);
				 }
				 return count;
			 });
		}


		public int UpdateByWhereIgnore(T model, string where, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
		{
			return Wrap(tran, () =>
			 {
				 int count = 0;
				 foreach (var item in TableList)
				 {
					 count += item.UpdateByWhereIgnore(model, where, fields, tran, timeout);
				 }
				 return count;
			 });
		}

		#endregion

		#region updateAsync

		public virtual async Task<int> UpdateAsync(T model, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = await GetTableByModelAsync(model);
			return await tb.UpdateAsync(model, fields, tran, timeout);
		}

		public virtual async Task UpdateAsync(IEnumerable<T> modelList, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
		{
			var task = await WrapAsync(tran, async () =>
			{
				var dict = GetTableByGroupModelList(modelList);
				foreach (var item in dict)
				{
					await item.Key.UpdateAsync(item.Value, fields, tran, timeout);
				}
			});
			await task;
		}

		public virtual async Task<int> UpdateIgnoreAsync(T model, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = await GetTableByModelAsync(model);
			return await tb.UpdateIgnoreAsync(model, fields, tran, timeout);
		}

		public virtual async Task UpdateIgnoreAsync(IEnumerable<T> modelList, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
		{
			var task = await WrapAsync(tran, async () =>
			{
				var dict = GetTableByGroupModelList(modelList);
				foreach (var item in dict)
				{
					await item.Key.UpdateIgnoreAsync(item.Value, fields, tran, timeout);
				}
			});
			await task;
		}

		public async Task<int> UpdateByWhereAsync(T model, string where, List<string> fields = null, DistributedTransaction tran = null, int? timeout = null)
		{
			var task = await WrapAsync(tran, async () =>
			{
				int count = 0;
				foreach (var item in TableList)
				{
					count += await item.UpdateByWhereAsync(model, where, fields, tran, timeout);
				}
				return count;
			});
			return await task;
		}


		public async Task<int> UpdateByWhereIgnoreAsync(T model, string where, List<string> fields, DistributedTransaction tran = null, int? timeout = null)
		{
			var task = await WrapAsync(tran, async () =>
			{
				int count = 0;
				foreach (var item in TableList)
				{
					count += await item.UpdateByWhereIgnoreAsync(model, where, fields, tran, timeout);
				}
				return count;
			});
			return await task;
		}

		#endregion

		#region delete

		public virtual int Delete(object id, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = GetTableById(id);
			return tb.Delete(id, tran, timeout);
		}

		public virtual int Delete(T model, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = GetTableByModel(model);
			return tb.Delete(model, tran, timeout);
		}

		public virtual int DeleteByIds(object ids, DistributedTransaction tran = null, int? timeout = null)
		{
			if (CommonUtil.ObjectIsEmpty(ids))
				return 0;
			return Wrap(tran, () =>
			 {
				 int count = 0;
				 var dict = GetTableByGroupIds(ids);
				 foreach (var item in dict)
				 {
					 count += item.Key.DeleteByIds(item.Value, tran, timeout);
				 }
				 return count;
			 });
		}

		public virtual void Delete(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null)
		{
			Wrap(tran, () =>
			 {
				 var dict = GetTableByGroupModelList(modelList);
				 foreach (var item in dict)
				 {
					 item.Key.Delete(item.Value, tran, timeout);
				 }
			 });
		}

		public int DeleteByWhere(string where, object param = null, DistributedTransaction tran = null, int? timeout = null)
		{
			return Wrap(tran, () =>
			 {
				 int count = 0;
				 foreach (var item in TableList)
				 {
					 count += item.DeleteByWhere(where, param, tran, timeout);
				 }
				 return count;
			 });
		}

		public int DeleteAll(DistributedTransaction tran = null, int? timeout = null)
		{
			return Wrap(tran, () =>
			 {
				 int count = 0;
				 foreach (var item in TableList)
				 {
					 count += item.DeleteAll(tran, timeout);
				 }
				 return count;
			 });
		}

		#endregion

		#region deleteAsync

		public virtual async Task<int> DeleteAsync(object id, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = await GetTableByIdAsync(id);
			return await tb.DeleteAsync(id, tran, timeout);
		}

		public virtual async Task<int> DeleteAsync(T model, DistributedTransaction tran = null, int? timeout = null)
		{
			var tb = await GetTableByModelAsync(model);
			return await tb.DeleteAsync(model, tran, timeout);
		}

		public virtual async Task<int> DeleteByIdsAsync(object ids, DistributedTransaction tran = null, int? timeout = null)
		{
			if (CommonUtil.ObjectIsEmpty(ids))
				return 0;
			var task = await WrapAsync(tran, async () =>
			{
				int count = 0;
				var dict = GetTableByGroupIds(ids);
				foreach (var item in dict)
				{
					count += await item.Key.DeleteByIdsAsync(item.Value, tran, timeout);
				}
				return count;
			});
			return await task;
		}

		public virtual async Task DeleteAsync(IEnumerable<T> modelList, DistributedTransaction tran = null, int? timeout = null)
		{
			var task = await WrapAsync(tran, async () =>
			{
				var dict = GetTableByGroupModelList(modelList);
				foreach (var item in dict)
				{
					await item.Key.DeleteAsync(item.Value, tran, timeout);
				}
			});
			await task;
		}

		public async Task<int> DeleteByWhereAsync(string where, object param = null, DistributedTransaction tran = null, int? timeout = null)
		{
			var task = await WrapAsync(tran, async () =>
			{
				int count = 0;
				foreach (var item in TableList)
				{
					count += await item.DeleteByWhereAsync(where, param, tran, timeout);
				}
				return count;
			});
			return await task;
		}

		public async Task<int> DeleteAllAsync(DistributedTransaction tran = null, int? timeout = null)
		{
			var task = await WrapAsync(tran, async () =>
			{
				int count = 0;
				foreach (var item in TableList)
				{
					count += await item.DeleteAllAsync(tran, timeout);
				}
				return count;
			});
			return await task;
		}

		#endregion

		#region query

		public virtual async Task<bool> ExistsAsync(object id)
		{
			var table = await GetTableByIdAsync(id);
			return await table.ExistsAsync(id);
		}

		public virtual async Task<bool> ExistsAsync(T model)
		{
			var table = await GetTableByModelAsync(model);
			return await table.ExistsAsync(model);
		}

        public async Task<bool> ExistsAsync(string where, object param = null, int? timeout = null)
        {
            var taskList = TableList.Select(s =>
            {
                return s.ExistsAsync(where, param, timeout: timeout);
            });
            var result = await Task.WhenAll(taskList);
            return result.Any(a => a == true);
        }

        public virtual async Task<T> GetByIdAsync(object id, string returnFields = null)
		{
			var table = await GetTableByIdAsync(id);
			return await table.GetByIdAsync(id, returnFields);
		}

		public virtual async Task<IEnumerable<T>> GetByIdsAsync(object ids, string returnFields = null)
		{
			if (CommonUtil.ObjectIsEmpty(ids))
				return Enumerable.Empty<T>();
			var dict = GetTableByGroupIds(ids);
			var taskList = dict.Select(s =>
			{
				return s.Key.GetByIdsAsync(s.Value, returnFields);
			});
			var result = await Task.WhenAll(taskList);
			return result.ConcatItem();
		}

		public async Task Truncate()
		{
			var taskList = TableList.Select(s =>
			{
				return s.TruncateAsync();
			});
			await Task.WhenAll(taskList);
		}

		#endregion

	}
}
